package com.dyna.gateway.filter.security;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.dyna.constants.SysConstant;
import com.dyna.constants.TopicConstant;
import com.dyna.core.exception.CustomException;
import com.dyna.core.utils.JsonUtil;
import com.dyna.core.utils.ObjectUtil;
import com.dyna.core.utils.StringUtil;
import com.dyna.core.utils.WebFrameworkUtil;
import com.dyna.domain.dto.LoginUser;
import com.dyna.domain.resp.R;
import com.dyna.domain.resp.ResultCode;
import com.dyna.gateway.config.properties.IgnoreWhiteProperties;
import com.dyna.gateway.domain.dto.OAuth2AccessTokenCheckRespDTO;
import com.dyna.gateway.utils.SecurityFrameworkUtils;
import com.dyna.utils.KafkaUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.Optional;

/**
 * 网关鉴权
 * @author admin
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    /**
     * CommonResult<OAuth2AccessTokenCheckRespDTO> 对应的 TypeReference 结果，用于解析 checkToken 的结果
     */
    private static final TypeReference<R<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE = new TypeReference<R<OAuth2AccessTokenCheckRespDTO>>() {};

    /**
     * 空的 LoginUser 的结果
     *
     * 用于解决如下问题：
     * 1. {@link # getLoginUser(ServerWebExchange, String)} 返回 Mono.empty() 时，会导致后续的 flatMap 无法进行处理的问题。
     * 2. {@link # buildUser(String)} 时，如果 Token 已经过期，返回 LOGIN_USER_EMPTY 对象，避免缓存无法刷新
     */
    private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();

    private final WebClient webClient;

    @Resource
    private IgnoreWhiteProperties ignoreWhite;

    public AuthFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
        // Q：为什么不使用 OAuth2TokenApi 进行调用？
        // A1：Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support
        // A2：校验 Token 的 API 需要使用到 header[tenant-id] 传递租户编号，暂时不想编写 RequestInterceptor 实现
        // 因此，这里采用 WebClient，通过 lbFunction 实现负载均衡
        this.webClient = WebClient.builder().filter(lbFunction).build();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 移除 login-user 的请求头，避免伪造模拟
        exchange = SecurityFrameworkUtils.removeLoginUser(exchange);
        String url = exchange.getRequest().getURI().getPath();
        if (StringUtil.matches(url, ignoreWhite.getWhites())) {
            return chain.filter(exchange);
        }
        // 情况一，如果没有 Token 令牌,直接返回错误
        String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
        if (StrUtil.isEmpty(token)) {
            return setUnauthorizedResponse(exchange, HttpStatus.UNAUTHORIZED, ResultCode.UN_AUTHORIZED.getCode(), SysConstant.TOKEN_NOT_NULL);
        }
        // 情况二，如果有 Token 令牌，则解析对应 userId、userType、tenantId 等字段，并通过 通过 Header 转发给服务
        // 重要说明：defaultIfEmpty 作用，保证 Mono.empty() 情况，可以继续执行 `flatMap 的 chain.filter(exchange)` 逻辑，避免返回给前端空的 Response！！
        ServerWebExchange finalExchange = exchange;
        Mono<LoginUser> loginUserMono;
        try {
            loginUserMono = getLoginUser(exchange, token);
        }catch (Exception e) {
            return setUnauthorizedResponse(exchange, HttpStatus.UNAUTHORIZED, ResultCode.UN_AUTHORIZED.getCode(), SysConstant.AUTHORIZATION_ERROR_INFO);
        }
        ServerWebExchange finalExchange1 = exchange;
        Mono<Void> mono = loginUserMono.defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> {
            // 1. 无用户，直接 filter 继续请求
            if (user == LOGIN_USER_EMPTY || ObjectUtil.isNull(user.getUserId())) {
                return setUnauthorizedResponse(finalExchange1, HttpStatus.UNAUTHORIZED, ResultCode.UN_AUTHORIZED.getCode(), SysConstant.AUTHORIZATION_ERROR_INFO);
            }
            // 2.2 将 user 并设置到 login-user 的请求头，使用 json 存储值
            ServerWebExchange newExchange = finalExchange.mutate()
                    .request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user)).build();
            return chain.filter(newExchange);
        });
        return mono;
    }

    private Mono<LoginUser> getLoginUser(ServerWebExchange exchange, String token) {
        // 从缓存中，获取 LoginUser
        Long tenantId = WebFrameworkUtil.getTenantId(exchange);
        // 缓存不存在，则请求远程服务
        String body = checkAccessToken(token);
        LoginUser remoteUser = buildUser(body);
        if (remoteUser != null) {
            // 非空，则进行缓存
            return Mono.just(remoteUser);
        }
        return Mono.empty();
    }

    private Mono<Void> setUnauthorizedResponse(ServerWebExchange exchange, HttpStatus status, int code, String msg) {
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.setStatusCode(status);
        log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            return bufferFactory.wrap(JSON.toJSONBytes(R.fail(code,msg)));
        }));
    }

    private String checkAccessToken(String accessToken) {
        String body = null;
        try {
            Mono<String> mono = webClient.get()
                    .uri("http://dyna-auth-application/token/check", uriBuilder -> uriBuilder.queryParam("accessToken", accessToken).build())
                    .retrieve().bodyToMono(String.class);
            body = mono.block();
        }catch (Exception e) {
            log.error("检查认证token请求失败，{}", e.getMessage(), e);
            throw new CustomException(e.getMessage());
        }
        return body;
    };

    private LoginUser buildUser(String body) {
        // 处理结果，结果不正确
        R<OAuth2AccessTokenCheckRespDTO> result = JsonUtil.parseObject(body, CHECK_RESULT_TYPE_REFERENCE);
        if (ObjectUtil.isNull(result) || !result.isSuccess() || Objects.equals(result.getCode(), HttpStatus.UNAUTHORIZED.value())) {
            // 特殊情况：令牌已经过期（code = 401），需要返回 LOGIN_USER_EMPTY，避免 Token 一直因为缓存，被误判为有效
            throw new CustomException(SysConstant.AUTHORIZATION_ERROR_INFO);
        }
        LoginUser loginUser = Optional.ofNullable(result.getData())
                .map(OAuth2AccessTokenCheckRespDTO::getUserInfo)
                .orElseThrow(() -> new CustomException(SysConstant.AUTHORIZATION_ERROR_INFO));
        // 刷新token过期时间
        KafkaUtil.send(TopicConstant.REFRESH_TOKEN_TOPIC, JSON.toJSONString(result.getData()));
        return loginUser;
    }

    @Override
    public int getOrder() {
        return -100; // 和 Spring Security Filter 的顺序对齐
    }

}